# !/usr/bin/env python3
# coding=utf8
"""
一键部署 + 一键更新,
Windows 和 Linux 都支持,
Linux 系统不用关心 ta-lib 的编译问题,
脚本会通过 get_vnpy_repo_root_folder/get_vnpy_venv_root_folder 锁定目录, 脚本在哪个位置执行都可以,
"""
import argparse
import dataclasses
import enum
import logging
import os
import pathlib
import platform
import re
import subprocess
from typing import Any, Dict, List, Set, Tuple, Type, Optional, Union, Callable, Sequence


def get_vnpy_venv_activate_script(vnpy_venv_root_folder: pathlib.Path = None) -> str:
    """
    vnpy的 venv虚拟环境里的 activate脚本的路径
    """
    if vnpy_venv_root_folder is None:
        vnpy_venv_root_folder: pathlib.Path = get_vnpy_venv_root_folder()

    match platform.system().lower():
        case "windows":
            activate_path: str = vnpy_venv_root_folder.joinpath("Scripts").joinpath("activate.bat").__str__()
        case "linux":
            activate_path: str = vnpy_venv_root_folder.joinpath("bin").joinpath("activate").__str__()
        case _:
            raise RuntimeError(f"unknown system={platform.system()}")

    return activate_path


def activate_script_validity_check():
    """
    这是一个单元测试: activate脚本的有效性检测
    """
    activate_script: str = get_vnpy_venv_activate_script()  # activate脚本的路径,
    command_1: str = rf"{activate_script} && python -m pip --version"  # 执行脚本 && 执行命令,

    commandItem_1: CommandItem = CommandItem(args=command_1, cwd=None)
    commandItem_1.run()

    command_2: str = rf"python -m pip --version"

    commandItem_2: CommandItem = CommandItem(args=command_2, cwd=None)
    commandItem_2.run()

    exit(0)


def xx1xx(vnpy_folder: pathlib.Path):
    """"""
    commands = [
        ["git", "status"],
        ["git", "branch", "-avv"],
        ["git", "push", "--force"],
    ]
    for args in commands:
        logging.info(" ".join(args))
        xxx: subprocess.CompletedProcess = subprocess.run(args, cwd=vnpy_folder, capture_output=True)
        logging.info(xxx.returncode)


def get_configured_repo_url() -> Dict[str, str]:
    """获取 配置在代码里的 代码库设置"""
    # vnpy              : 略,
    # vnpy_ctabacktester: 略,
    # vnpy_ctastrategy  : 略,
    # vnpy_sqlite       : 存储OrderData和TradeData数据,
    # vnpy_tdxdata      : 略(自建),
    # vnpy_datamanager  : 支持管理五秒线,
    # vnpy_zxtools      : 略(自建),
    # vnpy_datarecorder : Q：郑商所的品种都收不到14:59这一根bar, 存储OrderData和TradeData数据,
    # vnpy_rpcservice   : 为RPC服务注册了一些定制函数,
    # vnpy_ctacli       : 略(自建),
    # vnpy_ctp          : 交易接口, 定制日志, 用于留痕,
    url_branch_mapping: Dict[str, str] = {
        "https://gitee.com/zx9229/vnpy.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_ctabacktester.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_ctastrategy.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_sqlite.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_tdxdata.git": "",
        "https://gitee.com/zx9229/vnpy_datamanager.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_zxtools.git": "",
        "https://gitee.com/vnpy/vnpy_spreadtrading.git": "",
        "https://gitee.com/zx9229/vnpy_datarecorder.git": "remotes/origin/dev_zx",  # 它依赖了'vnpy_spreadtrading'
        "https://gitee.com/zx9229/vnpy_rpcservice.git": "remotes/origin/dev_zx",
        "https://gitee.com/zx9229/vnpy_ctacli.git": "",
        "https://gitee.com/zx9229/vnpy_ctp.git": "remotes/origin/dev_zx",
        "https://gitee.com/vnpy/vnpy_sopt.git": "",
    }
    return url_branch_mapping


def get_vnpy_venv_root_folder() -> pathlib.Path:
    """
    vnpy的 venv虚拟环境的 根目录
    """
    home: str = os.path.expanduser("~")  # Windows 和 Linux 都支持,

    root_folder: pathlib.Path = pathlib.Path(home).joinpath("vnpy_VeighNa")

    return root_folder


def get_vnpy_repo_root_folder(vnpy_venv_root_folder: pathlib.Path = None) -> pathlib.Path:
    """
    vnpy的 venv虚拟环境里的 repo代码库的 根目录
    """
    if vnpy_venv_root_folder is None:
        vnpy_venv_root_folder: pathlib.Path = get_vnpy_venv_root_folder()

    root_folder: pathlib.Path = vnpy_venv_root_folder.joinpath("gitee.com").joinpath("vnpy")

    return root_folder


def get_vnpy_venv_python(vnpy_venv_root_folder: pathlib.Path = None) -> str:
    """
    vnpy的 venv虚拟环境里的 python程序的 路径
    """
    if vnpy_venv_root_folder is None:
        vnpy_venv_root_folder: pathlib.Path = get_vnpy_venv_root_folder()

    assert vnpy_venv_root_folder.exists()

    match platform.system().lower():
        case "windows":
            python_path: str = vnpy_venv_root_folder.joinpath("Scripts").joinpath("python.exe").__str__()
        case "linux":
            python_path: str = vnpy_venv_root_folder.joinpath("bin").joinpath("python").__str__()
        case _:
            raise RuntimeError(f"unknown system={platform.system()}")

    return python_path


def get_default_python() -> str:
    """
    系统默认的 python环境的 命令
    """
    match platform.system().lower():
        case "windows":
            return "python"
        case "linux":
            return "python3"
        case _:
            raise RuntimeError(f"unknown system={platform.system()}")


def git_repository_name(url: str) -> str:
    """git代码库的目录名"""
    _git: str = ".git"

    # 虽然url是一个HTTP路径, 不是一个合法的路径入参, 不过刚好能被解析出来名字,
    name: str = pathlib.Path(url).name
    assert name.endswith(_git)

    folder: str = name[:-len(_git)]
    return folder


def get_vnpy_repositories(path: Union[pathlib.Path, str, None] = None) -> List[pathlib.Path]:
    """获取vnpy相关的本地代码库所在的路径"""
    if path is None:
        path = pathlib.Path(__file__).parent.parent.parent.parent

    elems: List[str] = os.listdir(path=path)
    items: List[pathlib.Path] = [pathlib.Path(path).joinpath(_) for _ in elems]
    items: List[pathlib.Path] = [_ for _ in items if _.is_dir()]
    items: List[pathlib.Path] = [_ for _ in items if _.name.startswith("vnpy")]
    items: List[pathlib.Path] = [_ for _ in items if _.joinpath(".git").is_dir()]

    items.sort()

    return items


def vnpy_git_pull_rebase() -> None:
    """"""
    repo_path_s: List[pathlib.Path] = get_vnpy_repositories(path=get_vnpy_repo_root_folder())

    commands: List[CommandItem] = [
        CommandItem(args=["git", "pull", "--rebase"], cwd=_) for _ in repo_path_s
    ]

    for command in commands:
        command.run()


def vnpy_venv_pip_install_dot() -> None:
    """
    为("vnpy_ctp", "vnpy_sopt")重新源码安装, 可以重复执行,
    如果已经执行过源码安装, 重新执行, 耗时不算多, 可以接受重新执行,
    """
    repo_path_s: List[pathlib.Path] = get_vnpy_repositories(path=get_vnpy_repo_root_folder())

    # 准备源码安装的包,
    repo_path_s: List[pathlib.Path] = [_ for _ in repo_path_s if _.name in ("vnpy_ctp", "vnpy_sopt")]
    repo_path_s.sort()
    if not repo_path_s:
        return

    # 寻找 venv 虚拟环境的根目录, (准备)
    vnpy_venv_root_folder: pathlib.Path = repo_path_s[0]
    assert "/vnpy_VeighNa/" in vnpy_venv_root_folder.as_posix()

    # 寻找 venv 虚拟环境的根目录,
    while vnpy_venv_root_folder.name != "vnpy_VeighNa":
        vnpy_venv_root_folder = vnpy_venv_root_folder.parent

    # 寻找 venv 虚拟环境的 python 可执行文件,
    vnpy_venv_python: pathlib.Path = get_vnpy_venv_python(vnpy_venv_root_folder=vnpy_venv_root_folder)

    commands: List[CommandItem] = [
        CommandItem(args=[vnpy_venv_python, "-m", "pip", "install", "."], cwd=_) for _ in repo_path_s
    ]

    for command in commands:
        command.run()


def vnpy_venv_run_install_script(index_url: Optional[str]) -> None:
    """
    执行"install.bat"/"install.sh"安装脚本, 疑似, 可以重复执行,
    如果依赖包已经安装过了, 重新执行脚本, 只是对安装包进行了一次检查, 耗时不算多, 可以接受重新执行,
    """
    repo_path_s: List[pathlib.Path] = get_vnpy_repositories(path=get_vnpy_repo_root_folder())

    # 寻找 vnpy.git 代码库, 因为"install.bat"/"install.sh"安装脚本就在里面,
    repo_path_s: List[pathlib.Path] = [_ for _ in repo_path_s if _.name in ("vnpy",)]
    repo_path_s.sort()
    if not repo_path_s:
        return

    # 寻找 venv 虚拟环境的根目录, (准备)
    vnpy_venv_root_folder: pathlib.Path = repo_path_s[0]
    assert "/vnpy_VeighNa/" in vnpy_venv_root_folder.as_posix()

    # 寻找 venv 虚拟环境的根目录,
    while vnpy_venv_root_folder.name != "vnpy_VeighNa":
        vnpy_venv_root_folder = vnpy_venv_root_folder.parent

    # 寻找 venv 虚拟环境的 python 可执行文件,
    vnpy_venv_python: pathlib.Path = get_vnpy_venv_python(vnpy_venv_root_folder=vnpy_venv_root_folder)

    assert len(repo_path_s) == 1
    # 寻找 vnpy.git 代码库,
    repo_path: pathlib.Path = repo_path_s[0]

    match platform.system().lower():
        case "windows":
            # dir 命令是cmd.exe或者powershell.exe才能理解的命令。
            # 在Windows环境下subprocess.Popen默认是不会调用cmd.exe的。所以需要指定。
            command_args: List[str] = ["cmd", "/C", "install.bat", vnpy_venv_python, index_url]
            if not index_url:
                command_args.pop()  # 删掉最后一个元素,

        case "linux":
            if True:
                with open(file=repo_path.joinpath("install.sh"), mode="r", encoding="utf8") as f:
                    text: str = f.read()
                text: str = re.sub(r"^[ \t]*(.*)# ubuntu_sudo$", r"sudo \1# ubuntu_sudo", text, flags=re.MULTILINE)
                with open(file=repo_path.joinpath("install.sh"), mode="w", encoding="utf8") as f:
                    f.write(text)

            command_args: List[str] = ["bash", "install.sh", vnpy_venv_python, index_url]
            if not index_url:
                command_args.pop()  # 删掉最后一个元素,

        case _:
            raise RuntimeError(f"unknown system={platform.system()}")

    commandItem: CommandItem = CommandItem(args=command_args, cwd=repo_path)
    commandItem.run()


@dataclasses.dataclass
class CommandItem:
    args: Union[List[str], pathlib.Path, str, bytes]
    cwd: Union[pathlib.Path, str, None]

    def run(self) -> subprocess.CompletedProcess:
        """"""
        spcp: subprocess.CompletedProcess = subprocess_run(args=self.args, cwd=self.cwd)

        isOK: bool = spcp.returncode == 0

        assert isOK
        return spcp


def subprocess_run(
    args: Union[str, bytes, os.PathLike, pathlib.Path, list, tuple],
    cwd: Union[pathlib.Path, str, None] = None,
    encoding: Optional[str] = None,
) -> subprocess.CompletedProcess:
    """"""
    logging.info('+' * 40)

    command: str = " ".join(args)
    message: str = f"[{command}], cwd=[{cwd}]"
    logging.info(message)

    spcp: subprocess.CompletedProcess = subprocess.run(
        args=args,
        cwd=cwd,
        encoding=encoding,
    )

    message: str = f"returncode=[{spcp.returncode}]"
    logging.info(message)

    logging.info('-' * 40)

    return spcp


@dataclasses.dataclass
class CheckItem:
    name: str
    path: pathlib.Path
    url: str
    branch: str


def subprocess_run_command(cmd: str, cwd: Union[str, pathlib.Path] = None, encoding=None) -> List[Union[bytes, str]]:
    """
    encoding: 为None时返回bytes, 为'utf8'/'gbk'等时返回str,
    """
    popen: subprocess.Popen = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, cwd=cwd, encoding=encoding, shell=True)
    lines: List[Union[bytes, str]] = popen.stdout.readlines()
    return lines


def branch_is_match(item: CheckItem):
    """本地分支的情况(当前的本地分支和对应的远程代码库的网址)和配置里的情况相同"""
    lines: List[str] = subprocess_run_command(cmd="git branch --show-current", cwd=item.path, encoding="utf8")
    assert len(lines) == 1
    branch_name: str = lines[0].strip()  # 本地分支名

    lines: List[str] = subprocess_run_command(cmd="git remote", cwd=item.path, encoding="utf8")
    assert len(lines) == 1
    remote_name: str = lines[0].strip()

    lines: List[str] = subprocess_run_command(cmd=f"git remote get-url --all {remote_name}", cwd=item.path, encoding="utf8")
    assert len(lines) == 1  # 一般情况下(fetch)和(push)的数据一样, 这里仅考虑该情况,
    remote_url: str = lines[0].strip()

    lines: List[str] = subprocess_run_command(cmd=f"git branch -vv --list {branch_name}", cwd=item.path, encoding="utf8")
    assert len(lines) == 1  # 找到被"[]"包围的内容并移除"[]"
    remote_branch: str = [_ for _ in lines[0].split(sep=" ") if _.startswith('[') and _.endswith(']')][0][1:-1]  # 远程分支名

    b_equal: bool = item.branch.endswith(remote_branch) if item.branch else remote_branch in ("origin/master", "origin/main")
    u_equal: bool = remote_url == item.url

    return b_equal and u_equal


def create_vnpy_venv(allow_ignore: bool) -> None:
    """
    创建虚拟环境, 可以重复执行,
    """
    # 如果目录已经存在, 就报错,
    if get_vnpy_venv_root_folder().exists():
        # 如果路径已存在, 就假定已经成功创建了这个虚拟环境, 那么就可以忽略它了,
        # 所以, 如果这个虚拟环境只处理了一半就报错, 然后程序退出了, 可以删除这个虚拟环境, 疑似这样就能重新执行了,
        if allow_ignore:
            return
        raise RuntimeError(f"目录 {get_vnpy_venv_root_folder()} 已存在, 无法创建虚拟环境!")

    # 虚拟环境的根目录,
    folder: str = get_vnpy_venv_root_folder().as_posix()

    command_args: List[str] = [get_default_python(), "-m", "venv", folder, "--system-site-packages"]

    commandItem: CommandItem = CommandItem(args=command_args, cwd=None)
    commandItem.run()


def process_one_repo_url(url: str, remote_branch: str, allow_ignore: bool) -> None:
    """
    克隆, 切分支, 基本设置, 生成pth文件,
    """
    need_track: bool = remote_branch.count("remotes/origin/") > 0

    # 代码库的根目录,
    repo_root_path: pathlib.Path = get_vnpy_repo_root_folder()
    repo_root_path.mkdir(parents=True, exist_ok=True)

    # 当前代码库的路径,
    curr_repo_path: pathlib.Path = get_vnpy_repo_root_folder().joinpath(git_repository_name(url))

    if curr_repo_path.exists():
        # 如果路径已存在, 就假定已经成功处理了这个代码库, 那么就可以忽略它了,
        # 所以, 如果这个代码库只处理了一半就报错, 然后程序退出了, 可以删除这个代码库, 疑似这样就能重新执行了,
        if allow_ignore:
            return
        raise RuntimeError(f"目录 {curr_repo_path} 已存在, 无法克隆代码库!")

    if True:
        # 寻找 venv 虚拟环境的 python 可执行文件,
        vnpy_venv_python: str = get_vnpy_venv_python()
        # 文件"pth_package.py"和 __file__ 在一起,
        pth_package_py: str = pathlib.Path(__file__).parent.joinpath("pth_package.py").__str__()
        # 计算"pth_package.py"接受的包路径,
        pkg_path: str = curr_repo_path.joinpath(git_repository_name(url)).__str__()

    commands: List[CommandItem] = [
        # 克隆代码块
        CommandItem(args=["git", "clone", url], cwd=repo_root_path),
        # 如果是 zx9229 的代码库, 就切换到 dev_zx 分支
        CommandItem(args=["git", "checkout", "--track", remote_branch], cwd=curr_repo_path),
        # 为代码库做基本设置
        CommandItem(args=["git", "config", "--local", "user.name", "Your Name"], cwd=curr_repo_path),
        # 为代码库做基本设置
        CommandItem(args=["git", "config", "--local", "user.email", "you@example.com"], cwd=curr_repo_path),
        # 为代码库做基本设置(尝试过"false"同时最终向"true"低头)
        CommandItem(args=["git", "config", "--local", "core.autocrlf", "true"], cwd=curr_repo_path),
        # 以 pth 文件的方式，将 代码库引入 site-packages
        CommandItem(args=[vnpy_venv_python, pth_package_py, f"--pkg_path={pkg_path}", "--dflt_opt=C"], cwd=None),
    ]
    # 如果需要追踪远程分支, 或者命令的参数里没有"--track", 就保留这样的命令, 用于后续执行,
    commands = [_ for _ in commands if need_track or _.args.count("--track") == 0]

    for command in commands:
        command.run()


def process_all_repo_url(allow_ignore: bool) -> None:
    """
    对每一个代码库执行(克隆, 切分支, 基本设置, 生成pth文件,)的操作, 可以重新执行,
    allow_ignore = True : 如果代码库已经存在就跳过它,
    """
    urls: Dict[str, str] = get_configured_repo_url()
    for url, branch in urls.items():
        process_one_repo_url(url=url, remote_branch=branch, allow_ignore=allow_ignore)


def one_step_deploy(idxurl: int, resume: bool) -> None:
    """一键部署"""
    match idxurl:
        case 1:
            index_url: Optional[str] = "https://pypi.tuna.tsinghua.edu.cn/simple"
        case _:
            index_url: Optional[str] = None

    create_vnpy_venv(allow_ignore=resume)
    process_all_repo_url(allow_ignore=resume)
    vnpy_venv_pip_install_dot()
    vnpy_venv_run_install_script(index_url=index_url)


def one_step_update() -> None:
    """一键更新"""
    vnpy_git_pull_rebase()
    vnpy_venv_pip_install_dot()


def one_step_chkgit() -> None:
    """校验git版本库"""
    repo_path_s: List[pathlib.Path] = get_vnpy_repositories(path=get_vnpy_repo_root_folder())

    mapping: Dict[str, CheckItem] = {_.name: CheckItem(name=_.name, path=_, url=None, branch=None) for _ in repo_path_s}

    url_branch_mapping: Dict[str, str] = get_configured_repo_url()

    for url, branch in url_branch_mapping.items():
        name: str = git_repository_name(url)
        item: Optional[CheckItem] = mapping.get(name, None)
        if item:
            item.url = url
            item.branch = branch
        else:
            item: CheckItem = CheckItem(name=name, path=None, url=url, branch=branch)
            mapping[item.name] = item

    for name, item in mapping.items():
        assert (item.name is not None) and (name == item.name)

    c_only: List[str] = [_.name for _ in mapping.values() if (_.url is not None) and (_.path is None)]
    l_only: List[str] = [_.name for _ in mapping.values() if (_.path is not None) and (_.url is None)]
    if c_only:
        print("config only", c_only)
    if l_only:
        print("local  only", l_only)

    for item in mapping.values():
        if (item.url is None) or (item.path is None):
            continue
        if not branch_is_match(item=item):
            print("branch not match", item.path)


class Action(enum.Enum):
    """"""
    NA = "NA"
    DEPLOY = "DEPLOY"
    UPDATE = "UPDATE"
    CHKGIT = "CHKGIT"


def main_function(action: Action, idxurl: int, resume: bool) -> None:
    """"""
    if False:
        pass
    elif action == Action.DEPLOY:
        one_step_deploy(idxurl=idxurl, resume=resume)
    elif action == Action.UPDATE:
        one_step_update()
    elif action == Action.CHKGIT:
        one_step_chkgit()
    else:
        raise RuntimeError(f"unknown action={action}")


def initialize_logging():
    """"""
    c_handler: logging.StreamHandler = logging.StreamHandler()
    f_handler: logging.FileHandler = logging.FileHandler(filename=f"{__file__}.log", mode="a", encoding="utf8")
    logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(message)s", handlers=[c_handler, f_handler])
    # logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(message)s")


# 如何重复执行一条命令直至运行成功？
# https://zhuanlan.zhihu.com/p/143764793
# 先执行一个命令, 如果这个命令失败了, 我们可以执行如下命令:
# until !!; do :; done
if __name__ == "__main__":
    initialize_logging()

    parser = argparse.ArgumentParser()
    parser.add_argument("--update", "-u", action="store_true", help="一键更新")
    parser.add_argument("--deploy", "-d", action="store_true", help="一键部署")  # 默认值是 False
    parser.add_argument("--chkgit", "-c", action="store_true", help="校验git版本库")
    parser.add_argument("--resume", "-r", action="store_true", help='配合"--deploy",(中断后)继续执行')
    parser.add_argument("--idxurl", "-i", type=int, default=1, help='配合"--deploy",pip换源,0不换源(官方源),1换1号源(清华源)')
    args = parser.parse_args()

    idxurl: int = args.idxurl
    resume: bool = args.resume
    action: Action = Action.NA

    if False:
        pass
    elif args.deploy:
        action: Action = Action.DEPLOY
    elif args.update:
        action: Action = Action.UPDATE
    elif args.chkgit:
        action: Action = Action.CHKGIT

    logging.info(f"action={action}, beg")
    main_function(action=action, idxurl=idxurl, resume=resume)
    logging.info(f"action={action}, end")

    exit(0)
